// Copyright (c) 2023 Gitpod GmbH. All rights reserved.
// Licensed under the GNU Affero General Public License (AGPL).
// See License.AGPL.txt in the project root for license information.

package main

import (
	"context"
	"errors"
	"fmt"
	"os"
	"strings"

	"github.com/authzed/spicedb/pkg/development"
	"github.com/authzed/spicedb/pkg/namespace"
	corev1 "github.com/authzed/spicedb/pkg/proto/core/v1"
	dev_v1 "github.com/authzed/spicedb/pkg/proto/developer/v1"
	implv1 "github.com/authzed/spicedb/pkg/proto/impl/v1"
	"github.com/authzed/spicedb/pkg/schemadsl/compiler"
	"gopkg.in/yaml.v2"
)

type Schema struct {
	Schema string `yaml:"schema"`
}

func main() {
	schema := GetCompiledSchema()
	fmt.Print(GenerateDefinition(schema))
}

func GetCompiledSchema() *compiler.CompiledSchema {
	// read schemaFile
	b, err := os.ReadFile("../schema/schema.yaml")
	if err != nil {
		panic(err)
	}

	var schema Schema
	err = yaml.Unmarshal(b, &schema)
	if err != nil {
		panic(err)
	}

	devCtx, devErrs, err := development.NewDevContext(context.Background(), &dev_v1.RequestContext{
		Schema:        schema.Schema,
		Relationships: nil,
	})
	if err != nil {
		panic(err)
	}
	if devErrs != nil {
		panic(errors.New(devErrs.InputErrors[0].Message))
	}
	return devCtx.CompiledSchema
}

func firstUpper(in string) string {
	return strings.ToUpper(string(in[0])) + in[1:]
}

func GenerateDefinition(schema *compiler.CompiledSchema) string {

	resource := "export type ResourceType ="
	resourceTypes := "export const AllResourceTypes: ResourceType[] = ["
	relation := "export type Relation ="
	permission := "export type Permission ="
	other := ""
	fluentApi := ""

	// list definitions
	for _, def := range schema.ObjectDefinitions {
		// make sure the first character is upper case
		simpleName := firstUpper(def.Name)
		resourceTypeName := simpleName + "ResourceType"
		resource += "\n  | " + resourceTypeName + ""
		resourceTypes += "\n  \"" + def.Name + "\","
		other += "\nexport type " + resourceTypeName + " = \"" + def.Name + "\";\n"
		fluentApi += "\n" + generateFluentAPI(def)
		// check if relations exists
		hasRelations := false
		for _, rel := range def.Relation {
			if namespace.GetRelationKind(rel) == implv1.RelationMetadata_RELATION {
				hasRelations = true
				break
			}
		}
		if hasRelations {
			relation += "\n  | " + simpleName + "Relation"
			other += "\nexport type " + simpleName + "Relation ="
			for _, rel := range def.Relation {
				if namespace.GetRelationKind(rel) == implv1.RelationMetadata_RELATION {
					other += "\n     | \"" + rel.Name + "\""
				}
			}
			other += ";\n"
		}
		// check if permissions exists
		hasPermissions := false
		for _, rel := range def.Relation {
			if namespace.GetRelationKind(rel) == implv1.RelationMetadata_PERMISSION {
				hasPermissions = true
				break
			}
		}
		if hasPermissions {
			permission += "\n  | " + simpleName + "Permission"
			// permissions
			other += "\nexport type " + simpleName + "Permission ="
			for _, rel := range def.Relation {
				if namespace.GetRelationKind(rel) == implv1.RelationMetadata_PERMISSION {
					other += "\n     | \"" + rel.Name + "\""
				}
			}
			other += ";\n"
		}
	}

	return `
		/**
		* Copyright (c) 2023 Gitpod GmbH. All rights reserved.
		* Licensed under the GNU Affero General Public License (AGPL).
		* See License.AGPL.txt in the project root for license information.
		*/

		// This file is generated by the spicedb/codegen/codegen.go. Do not edit manually.

		import { v1 } from "@authzed/authzed-node";

		export const InstallationID = "1";

		` + resource + `;

		` + resourceTypes + `
		];

		` + relation + `;

		` + permission + `;

		` + other + `

		export const rel = {
			` + fluentApi + `
		};
		`
}

func generateFluentAPI(def *corev1.NamespaceDefinition) string {
	ifElse := func(cond bool, ifTrue string, ifFalse string) string {
		if cond {
			return ifTrue
		}
		return ifFalse
	}

	relations := ""
	for _, rel := range def.Relation {
		if namespace.GetRelationKind(rel) != implv1.RelationMetadata_RELATION {
			continue
		}

		objects := ``
		for _, t := range rel.TypeInformation.AllowedDirectRelations {
			if t.GetPublicWildcard() != nil {
				objects += `get any` + firstUpper(t.Namespace) + `() {
					return {
						...result2,
						subject: {
							object: {
								objectType: "` + t.Namespace + `",
								objectId: "*",
							},

						},
					} as v1.Relationship;
				},`
			} else {
				hasRelation := t.GetRelation() != "..."
				name := ifElse(hasRelation, t.Namespace+"_"+t.GetRelation(), t.Namespace)
				objects += `
				` + ifElse(t.Namespace == "installation",
					"get "+name+"()",
					name+"(objectId: string)") + ` {
					return {
						...result2,
						subject: {
							object: {
								objectType: "` + t.Namespace + `",
								objectId: ` + ifElse(t.Namespace == "installation", `InstallationID`, "objectId") + `,
							},
							` + ifElse(hasRelation, `optionalRelation: "`+t.GetRelation()+`",`, "") + `
						},
					} as v1.Relationship;
				},`
			}
		}

		relations += `
			get ` + rel.Name + `() {
				const result2 = {
                    ...result,
                    relation: "` + rel.Name + `",
                };
				return {
					` + objects + `
				};
			},

		`
	}

	return `
	` + ifElse(def.Name == "installation",
		`get `+def.Name+`() {`,
		``+def.Name+`(id: string) {`) + `
		const result: Partial<v1.Relationship> = {
			resource: {
				objectType: "` + def.Name + `",
				objectId: ` + ifElse(def.Name == "installation", `InstallationID`, "id") + `
			},
		};
		return {
			` + relations + `
		};
	},
`
}
